﻿
namespace Patterns4Net.Core.CecilExtensions
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using Mono.Cecil;
    using Mono.Cecil.Cil;
    using Mono.Cecil.Rocks;

    /// <summary>
    /// Provides extension methods for <see cref="MethodReference"/>,
    /// <see cref="MethodDefinition"/> and related classes.
    /// </summary>
    public static class MethodExtensions
    {
        /// <summary>
        /// Returns <c>true</c> if the method has no return type.
        /// </summary>
        public static bool IsVoid(this MethodReturnType methodReturnType)
        {
            Contract.Requires(methodReturnType != null);
            return methodReturnType.ReturnType.FullName == "System.Void";
        }

        /// <summary>
        /// Returns <c>true</c> if the method has no return type.
        /// </summary>
        public static bool IsVoid(this MethodReference methodReference)
        {
            Contract.Requires(methodReference != null);
            return methodReference.MethodReturnType.IsVoid();
        }

        /// <summary>
        /// Returns <c>true</c> if the method is static.
        /// </summary>
        public static bool IsStatic(this MethodReference method)
        {
            Contract.Requires(method != null);
            return method.HasThis == false;
        }

        /// <summary>
        /// Returns <c>true</c> if the method has the <paramref name="type"/> as a return type.
        /// </summary>
        public static bool Returns(this MethodReference methodReference, TypeReference type)
        {
            Contract.Requires(methodReference != null);
            return methodReference.ReturnType.IsEqual(type);
        }

        /// <summary>
        /// Returns <c>true</c> if the method has the <paramref name="type"/> as a return type.
        /// </summary>
        public static bool Returns(this MethodReference methodReference, Type type)
        {
            Contract.Requires(methodReference != null);
            return methodReference.ReturnType.IsEqual(type);
        }

        /// <summary>
        /// Returns <c>true</c> if <paramref name="method"/> can be overridden in super type. 
        /// A method can be overridden if it is not final and is virtual or abstract.
        /// </summary>
        public static bool IsOverriddeable(this MethodDefinition method)
        {
            Contract.Requires(method != null);
            return method.IsFinal == false && (method.IsVirtual || method.IsAbstract);
        }

        /// <summary>
        /// Returns <c>true</c> if <paramref name="method"/> overrides a base type method, 
        /// or if it is implementation of a method from an interface.
        /// </summary>
        public static bool OverridesBaseMethodOrImplementsInterface(this MethodDefinition method)
        {
            return method.OverridesBaseMethod() ||
                method.Overrides.Any() ||
                method.DeclaringType.Interfaces.Any(i =>
                    {
                        var resolved = i.Resolve();
                        return resolved != null &&
                            resolved.Methods.Any(m => m.SignatureMatches(method));
                    });
        }

        /// <summary>
        /// Returns <c>true</c> if the method overrides a method from the base class.
        /// </summary>
        public static bool OverridesBaseMethod(this MethodDefinition method)
        {
            Contract.Requires(method != null);
            return method.GetBaseMethod() != method;
        }

        /// <summary>
        /// Returns <c>true</c> if the method overrides the <paramref name="overridden"/> method.
        /// </summary>
        public static bool Overrides(this MethodDefinition method, MethodReference overridden)
        {
            Contract.Requires(method != null);
            Contract.Requires(overridden != null);
            Contract.Assume(method.Overrides != null);
            Contract.Assume(method.GetOriginalBaseMethod() != null);

            bool explicitIfaceImplementation = method.Overrides.Any(overrides => overrides.IsEqual(overridden));
            if (explicitIfaceImplementation)
            {
                return true;
            }

            if (IsImplicitInterfaceImplementation(method, overridden))
            {
                return true;
            }

            // new slot method cannot override any base classes' method by convention:
            if (method.IsNewSlot)
            {
                return false;
            }

            // check base-type overrides using Cecil's helper method GetOriginalBaseMethod()
            return method.GetOriginalBaseMethod().IsEqual(overridden);
        }

        /// <summary>
        /// Implicit interface implementations are based only on method's name and signature equivalence.
        /// </summary>
        private static bool IsImplicitInterfaceImplementation(MethodDefinition method, MethodReference overridden)
        {
            Contract.Requires(method != null);
            Contract.Requires(overridden != null);

            // check that the 'overridden' method is iface method and the iface is implemented by method.DeclaringType
            if (overridden.DeclaringType.SafeResolve().IsInterface == false ||
                method.DeclaringType.Interfaces.None(i => i.IsEqual(overridden.DeclaringType)))
            {
                return false;
            }

            // check whether the type contains some other explicit implementation of the method
            if (method.DeclaringType.Methods.SelectMany(m => m.Overrides).Any(m => m.IsEqual(overridden)))
            {
                // explicit implementation -> no implicit implementation possible
                return false;
            }

            // now it is enough to just match the signatures and names:
            return method.Name == overridden.Name && method.SignatureMatches(overridden);
        }

        /// <summary>
        /// Returns <c>true</c> if the method invokes any method declared in the <paramref name="type"/>.
        /// </summary>
        public static bool CallsAnyOf(this MethodDefinition method, TypeDefinition type)
        {
            return method.GetMethodCalls().Any(call => type.Methods.Any(m => call.Method.IsEqual(m)));
        }

        /// <summary>
        /// Returns an enumeration with information about method invocations in the body of the method.
        /// </summary>
        public static IEnumerable<MethodCall> GetMethodCalls(this MethodDefinition method)
        {
            Contract.Requires(method != null);
            Contract.Ensures(Contract.Result<IEnumerable<MethodCall>>() != null);
            if (method.HasBody == false)
            {
                yield break;
            }

            method.Body.SimplifyMacros();
            var stackStates = StackObserver.CreateStackStates(method);
            for (int i = 0; i < method.Body.Instructions.Count; i++)
            {
                var currentInstruction = method.Body.Instructions[i];
                if (currentInstruction.IsCallInstruction())
                {
                    var calledMethod = (MethodReference)currentInstruction.Operand;
                    StackItem thisParameter = null;
                    if (calledMethod.HasThis)
                    {
                        thisParameter = stackStates[i - 1].Reverse().Skip(calledMethod.Parameters.Count).First();
                    }

                    var parameters = stackStates[i - 1].Reverse().Take(calledMethod.Parameters.Count).Reverse().ToArray();
                    yield return new MethodCall(calledMethod, thisParameter, parameters);
                }
            }
        }

        /// <summary>
        /// Returns an enumeration with information about fields access in the body of the method.
        /// </summary>
        /// <remarks>
        /// <para>Field access is assignment to a field, or reading of the value of a field.
        /// A field access is performed by instructions ldfld, stfld, ldsfld and stsfld.</para>
        /// </remarks>
        public static IEnumerable<FieldAccess> GetFieldAccesses(this MethodDefinition method)
        {
            Contract.Requires(method != null);
            Contract.Ensures(Contract.Result<IEnumerable<FieldAccess>>() != null);
            if (method.HasBody == false)
            {
                yield break;
            }

            method.Body.SimplifyMacros();
            var stackStates = StackObserver.CreateStackStates(method);
            for (int i = 0; i < method.Body.Instructions.Count; i++)
            {
                var currentInstruction = method.Body.Instructions[i];
                if (currentInstruction.OpCode == OpCodes.Stfld || 
                    currentInstruction.OpCode == OpCodes.Stsfld)
                {
                    var field = (FieldReference)currentInstruction.Operand;
                    yield return new FieldAccess(field, stackStates[i - 1].Last());
                }
                else if (currentInstruction.OpCode == OpCodes.Ldfld ||
                        currentInstruction.OpCode == OpCodes.Ldsfld)
                {
                    var field = (FieldReference)currentInstruction.Operand;
                    yield return new FieldAccess(field, null);
                }
            }
        }

        /// <summary>
        /// Returns an enumeration with information about constructors invocations in the body of the method.
        /// </summary>
        public static IEnumerable<ConstructorCall> GetConstructorCalls(this MethodDefinition method)
        {
            Contract.Requires(method != null);
            Contract.Ensures(Contract.Result<IEnumerable<ConstructorCall>>() != null);
            if (method.HasBody == false)
            {
                yield break;
            }

            method.Body.SimplifyMacros();
            var stackStates = StackObserver.CreateStackStates(method);
            for (int i = 0; i < method.Body.Instructions.Count; i++)
            {
                var currentInstruction = method.Body.Instructions[i];
                if (currentInstruction.OpCode == OpCodes.Newobj)
                {
                    var ctor = (MethodReference)currentInstruction.Operand;
                    var parameters = stackStates[i - 1].Reverse().Take(ctor.Parameters.Count).Reverse().ToArray();
                    yield return new ConstructorCall(ctor, parameters);
                }
            }            
        }

        /// <summary>
        /// Returns <c>true</c> if parameters types, parameters order and return type of given two methods match.
        /// </summary>
        public static bool SignatureMatches(this MethodReference m1, MethodReference m2)
        {
            Contract.Requires(m1 != null);
            Contract.Requires(m2 != null);

            if ((m1.IsVoid() == false && m2.IsVoid()) ||
                (m1.IsVoid() && m2.IsVoid() == false))
            {
                return false;
            }

            if (m1.IsVoid() == false && m2.IsVoid() == false &&
                m1.ReturnType.IsEqual(m2.ReturnType) == false)
            {
                return false;
            }

            for (int i = 0; i < m1.Parameters.Count; i++)
            {
                if (m1.Parameters[i].ParameterType.IsEqual(m2.Parameters[i].ParameterType) == false)
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Conventional method for comparing two instances of <see cref="MethodReference"/>.
        /// </summary>
        public static bool IsEqual(this MethodReference method1, MethodReference method2)
        {
            Contract.Requires(method1 != null);
            Contract.Requires(method2 != null);
            return method1.Name == method2.Name && method1.DeclaringType.IsEqual(method2.DeclaringType);
        }
    }
}
